[GitHub API v3] 指定のPull RequestのLinked Issueを取得する
こんにちは、CX事業本部 IoT事業部の若槻です。
最近、Pull Requestでの開発の自動化のために、「Pull Requestの変更をデプロイ(≠マージ)をしたらLinked Issueに必要な情報を書き込む」という処理を実装する必要がありました。(あまり無いパターンかも知れませんが。)
そこで今回は、GitHub API v3で指定のPull RequestのLinked Issueを取得する方法を確認してみました。
Linked Issueとは?
Pull RequestのBodyにclose #{Issue番号}
と記載すると、そのIssueはPull Requestの「Linked Issue」となります。このPull RequestをCloseすると、Linked Issueも合わせてCloseされるため、Issueで機能開発やバグフィックスを管理する場合に便利です。
Linkされた側のIssueではLink元の「Linked Puull Request」が確認できます。
やってみた
アクセストークンの発行
まだの場合は下記から発行します。
スコープはrepo
を指定します。(repo:status
だけだと後述のAPI操作で権限不足となります。)
Pull Requestを取得する
指定のPull Requestの情報は、GitHub API v3の下記のエンドポイントを使用すれば取得可能です。
GET /repos/{owner}/{repo}/pulls/{pull_number}
取得してみます。
% GITHUB_ACCESS_AOKEN=##GITHUB_ACCESS_AOKEN## % OWNER=cm-rwakatsuki % REPO=test % pull_number=22 % result=$(curl \ -H "Accept: application/vnd.github.v3+json" \ -H "Authorization: token ${GITHUB_ACCESS_AOKEN}" \ "https://api.github.com/repos/${OWNER}/${REPO}/pulls/${pull_number}")
取得結果のJSONは下記のようになります。
$ echo ${result} > get_pull_result.json
{ "url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22", "id": 723439756, "node_id": "MDExOlB1bGxSZXF1ZXN0NzIzNDM5NzU2", "html_url": "https://github.com/cm-rwakatsuki/test/pull/22", "diff_url": "https://github.com/cm-rwakatsuki/test/pull/22.diff", "patch_url": "https://github.com/cm-rwakatsuki/test/pull/22.patch", "issue_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/22", "number": 22, "state": "open", "locked": false, "title": "Develop", "user": { "login": "cm-rwakatsuki", "id": 57384023, "node_id": "MDQ6VXNlcjU3Mzg0MDIz", "avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4", "gravatar_id": "", "url": "https://api.github.com/users/cm-rwakatsuki", "html_url": "https://github.com/cm-rwakatsuki", "followers_url": "https://api.github.com/users/cm-rwakatsuki/followers", "following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}", "gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}", "starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions", "organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs", "repos_url": "https://api.github.com/users/cm-rwakatsuki/repos", "events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}", "received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events", "type": "User", "site_admin": false }, "body": "close #23", "created_at": "2021-08-31T14:37:26Z", "updated_at": "2021-08-31T15:01:54Z", "closed_at": null, "merged_at": null, "merge_commit_sha": "6401b9ab1edc6e700343b26ab1132bf7f1234670", "assignee": null, "assignees": [ ], "requested_reviewers": [ ], "requested_teams": [ ], "labels": [ ], "milestone": null, "draft": false, "commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22/commits", "review_comments_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22/comments", "review_comment_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/comments{/number}", "comments_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/22/comments", "statuses_url": "https://api.github.com/repos/cm-rwakatsuki/test/statuses/5e34d6c373c794e5142909259659b7fc03f4bb30", "head": { "label": "cm-rwakatsuki:develop", "ref": "develop", "sha": "5e34d6c373c794e5142909259659b7fc03f4bb30", "user": { "login": "cm-rwakatsuki", "id": 57384023, "node_id": "MDQ6VXNlcjU3Mzg0MDIz", "avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4", "gravatar_id": "", "url": "https://api.github.com/users/cm-rwakatsuki", "html_url": "https://github.com/cm-rwakatsuki", "followers_url": "https://api.github.com/users/cm-rwakatsuki/followers", "following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}", "gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}", "starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions", "organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs", "repos_url": "https://api.github.com/users/cm-rwakatsuki/repos", "events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}", "received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events", "type": "User", "site_admin": false }, "repo": { "id": 302627041, "node_id": "MDEwOlJlcG9zaXRvcnkzMDI2MjcwNDE=", "name": "test", "full_name": "cm-rwakatsuki/test", "private": true, "owner": { "login": "cm-rwakatsuki", "id": 57384023, "node_id": "MDQ6VXNlcjU3Mzg0MDIz", "avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4", "gravatar_id": "", "url": "https://api.github.com/users/cm-rwakatsuki", "html_url": "https://github.com/cm-rwakatsuki", "followers_url": "https://api.github.com/users/cm-rwakatsuki/followers", "following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}", "gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}", "starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions", "organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs", "repos_url": "https://api.github.com/users/cm-rwakatsuki/repos", "events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}", "received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events", "type": "User", "site_admin": false }, "html_url": "https://github.com/cm-rwakatsuki/test", "description": null, "fork": false, "url": "https://api.github.com/repos/cm-rwakatsuki/test", "forks_url": "https://api.github.com/repos/cm-rwakatsuki/test/forks", "keys_url": "https://api.github.com/repos/cm-rwakatsuki/test/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/cm-rwakatsuki/test/collaborators{/collaborator}", "teams_url": "https://api.github.com/repos/cm-rwakatsuki/test/teams", "hooks_url": "https://api.github.com/repos/cm-rwakatsuki/test/hooks", "issue_events_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/events{/number}", "events_url": "https://api.github.com/repos/cm-rwakatsuki/test/events", "assignees_url": "https://api.github.com/repos/cm-rwakatsuki/test/assignees{/user}", "branches_url": "https://api.github.com/repos/cm-rwakatsuki/test/branches{/branch}", "tags_url": "https://api.github.com/repos/cm-rwakatsuki/test/tags", "blobs_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/blobs{/sha}", "git_tags_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/tags{/sha}", "git_refs_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/refs{/sha}", "trees_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/trees{/sha}", "statuses_url": "https://api.github.com/repos/cm-rwakatsuki/test/statuses/{sha}", "languages_url": "https://api.github.com/repos/cm-rwakatsuki/test/languages", "stargazers_url": "https://api.github.com/repos/cm-rwakatsuki/test/stargazers", "contributors_url": "https://api.github.com/repos/cm-rwakatsuki/test/contributors", "subscribers_url": "https://api.github.com/repos/cm-rwakatsuki/test/subscribers", "subscription_url": "https://api.github.com/repos/cm-rwakatsuki/test/subscription", "commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/commits{/sha}", "git_commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/commits{/sha}", "comments_url": "https://api.github.com/repos/cm-rwakatsuki/test/comments{/number}", "issue_comment_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/comments{/number}", "contents_url": "https://api.github.com/repos/cm-rwakatsuki/test/contents/{+path}", "compare_url": "https://api.github.com/repos/cm-rwakatsuki/test/compare/{base}...{head}", "merges_url": "https://api.github.com/repos/cm-rwakatsuki/test/merges", "archive_url": "https://api.github.com/repos/cm-rwakatsuki/test/{archive_format}{/ref}", "downloads_url": "https://api.github.com/repos/cm-rwakatsuki/test/downloads", "issues_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues{/number}", "pulls_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls{/number}", "milestones_url": "https://api.github.com/repos/cm-rwakatsuki/test/milestones{/number}", "notifications_url": "https://api.github.com/repos/cm-rwakatsuki/test/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/cm-rwakatsuki/test/labels{/name}", "releases_url": "https://api.github.com/repos/cm-rwakatsuki/test/releases{/id}", "deployments_url": "https://api.github.com/repos/cm-rwakatsuki/test/deployments", "created_at": "2020-10-09T11:54:38Z", "updated_at": "2021-07-14T16:52:42Z", "pushed_at": "2021-08-31T14:37:26Z", "git_url": "git://github.com/cm-rwakatsuki/test.git", "ssh_url": "git@github.com:cm-rwakatsuki/test.git", "clone_url": "https://github.com/cm-rwakatsuki/test.git", "svn_url": "https://github.com/cm-rwakatsuki/test", "homepage": null, "size": 61, "stargazers_count": 0, "watchers_count": 0, "language": "Shell", "has_issues": true, "has_projects": true, "has_downloads": true, "has_wiki": true, "has_pages": false, "forks_count": 0, "mirror_url": null, "archived": false, "disabled": false, "open_issues_count": 8, "license": null, "forks": 0, "open_issues": 8, "watchers": 0, "default_branch": "main" } }, "base": { "label": "cm-rwakatsuki:main", "ref": "main", "sha": "50adcafd9bd8b662af5ee34c0456663e34cd940d", "user": { "login": "cm-rwakatsuki", "id": 57384023, "node_id": "MDQ6VXNlcjU3Mzg0MDIz", "avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4", "gravatar_id": "", "url": "https://api.github.com/users/cm-rwakatsuki", "html_url": "https://github.com/cm-rwakatsuki", "followers_url": "https://api.github.com/users/cm-rwakatsuki/followers", "following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}", "gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}", "starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions", "organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs", "repos_url": "https://api.github.com/users/cm-rwakatsuki/repos", "events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}", "received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events", "type": "User", "site_admin": false }, "repo": { "id": 302627041, "node_id": "MDEwOlJlcG9zaXRvcnkzMDI2MjcwNDE=", "name": "test", "full_name": "cm-rwakatsuki/test", "private": true, "owner": { "login": "cm-rwakatsuki", "id": 57384023, "node_id": "MDQ6VXNlcjU3Mzg0MDIz", "avatar_url": "https://avatars.githubusercontent.com/u/57384023?v=4", "gravatar_id": "", "url": "https://api.github.com/users/cm-rwakatsuki", "html_url": "https://github.com/cm-rwakatsuki", "followers_url": "https://api.github.com/users/cm-rwakatsuki/followers", "following_url": "https://api.github.com/users/cm-rwakatsuki/following{/other_user}", "gists_url": "https://api.github.com/users/cm-rwakatsuki/gists{/gist_id}", "starred_url": "https://api.github.com/users/cm-rwakatsuki/starred{/owner}{/repo}", "subscriptions_url": "https://api.github.com/users/cm-rwakatsuki/subscriptions", "organizations_url": "https://api.github.com/users/cm-rwakatsuki/orgs", "repos_url": "https://api.github.com/users/cm-rwakatsuki/repos", "events_url": "https://api.github.com/users/cm-rwakatsuki/events{/privacy}", "received_events_url": "https://api.github.com/users/cm-rwakatsuki/received_events", "type": "User", "site_admin": false }, "html_url": "https://github.com/cm-rwakatsuki/test", "description": null, "fork": false, "url": "https://api.github.com/repos/cm-rwakatsuki/test", "forks_url": "https://api.github.com/repos/cm-rwakatsuki/test/forks", "keys_url": "https://api.github.com/repos/cm-rwakatsuki/test/keys{/key_id}", "collaborators_url": "https://api.github.com/repos/cm-rwakatsuki/test/collaborators{/collaborator}", "teams_url": "https://api.github.com/repos/cm-rwakatsuki/test/teams", "hooks_url": "https://api.github.com/repos/cm-rwakatsuki/test/hooks", "issue_events_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/events{/number}", "events_url": "https://api.github.com/repos/cm-rwakatsuki/test/events", "assignees_url": "https://api.github.com/repos/cm-rwakatsuki/test/assignees{/user}", "branches_url": "https://api.github.com/repos/cm-rwakatsuki/test/branches{/branch}", "tags_url": "https://api.github.com/repos/cm-rwakatsuki/test/tags", "blobs_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/blobs{/sha}", "git_tags_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/tags{/sha}", "git_refs_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/refs{/sha}", "trees_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/trees{/sha}", "statuses_url": "https://api.github.com/repos/cm-rwakatsuki/test/statuses/{sha}", "languages_url": "https://api.github.com/repos/cm-rwakatsuki/test/languages", "stargazers_url": "https://api.github.com/repos/cm-rwakatsuki/test/stargazers", "contributors_url": "https://api.github.com/repos/cm-rwakatsuki/test/contributors", "subscribers_url": "https://api.github.com/repos/cm-rwakatsuki/test/subscribers", "subscription_url": "https://api.github.com/repos/cm-rwakatsuki/test/subscription", "commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/commits{/sha}", "git_commits_url": "https://api.github.com/repos/cm-rwakatsuki/test/git/commits{/sha}", "comments_url": "https://api.github.com/repos/cm-rwakatsuki/test/comments{/number}", "issue_comment_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues/comments{/number}", "contents_url": "https://api.github.com/repos/cm-rwakatsuki/test/contents/{+path}", "compare_url": "https://api.github.com/repos/cm-rwakatsuki/test/compare/{base}...{head}", "merges_url": "https://api.github.com/repos/cm-rwakatsuki/test/merges", "archive_url": "https://api.github.com/repos/cm-rwakatsuki/test/{archive_format}{/ref}", "downloads_url": "https://api.github.com/repos/cm-rwakatsuki/test/downloads", "issues_url": "https://api.github.com/repos/cm-rwakatsuki/test/issues{/number}", "pulls_url": "https://api.github.com/repos/cm-rwakatsuki/test/pulls{/number}", "milestones_url": "https://api.github.com/repos/cm-rwakatsuki/test/milestones{/number}", "notifications_url": "https://api.github.com/repos/cm-rwakatsuki/test/notifications{?since,all,participating}", "labels_url": "https://api.github.com/repos/cm-rwakatsuki/test/labels{/name}", "releases_url": "https://api.github.com/repos/cm-rwakatsuki/test/releases{/id}", "deployments_url": "https://api.github.com/repos/cm-rwakatsuki/test/deployments", "created_at": "2020-10-09T11:54:38Z", "updated_at": "2021-07-14T16:52:42Z", "pushed_at": "2021-08-31T14:37:26Z", "git_url": "git://github.com/cm-rwakatsuki/test.git", "ssh_url": "git@github.com:cm-rwakatsuki/test.git", "clone_url": "https://github.com/cm-rwakatsuki/test.git", "svn_url": "https://github.com/cm-rwakatsuki/test", "homepage": null, "size": 61, "stargazers_count": 0, "watchers_count": 0, "language": "Shell", "has_issues": true, "has_projects": true, "has_downloads": true, "has_wiki": true, "has_pages": false, "forks_count": 0, "mirror_url": null, "archived": false, "disabled": false, "open_issues_count": 8, "license": null, "forks": 0, "open_issues": 8, "watchers": 0, "default_branch": "main" } }, "_links": { "self": { "href": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22" }, "html": { "href": "https://github.com/cm-rwakatsuki/test/pull/22" }, "issue": { "href": "https://api.github.com/repos/cm-rwakatsuki/test/issues/22" }, "comments": { "href": "https://api.github.com/repos/cm-rwakatsuki/test/issues/22/comments" }, "review_comments": { "href": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22/comments" }, "review_comment": { "href": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/comments{/number}" }, "commits": { "href": "https://api.github.com/repos/cm-rwakatsuki/test/pulls/22/commits" }, "statuses": { "href": "https://api.github.com/repos/cm-rwakatsuki/test/statuses/5e34d6c373c794e5142909259659b7fc03f4bb30" } }, "author_association": "OWNER", "auto_merge": null, "active_lock_reason": null, "merged": false, "mergeable": true, "rebaseable": true, "mergeable_state": "clean", "merged_by": null, "comments": 0, "review_comments": 0, "maintainer_can_modify": false, "commits": 7, "additions": 0, "deletions": 0, "changed_files": 1 }
指定のPull Requestの情報が取得できました。
Pull RequestのLinked Issueを取得
あとは前述のPull Requestの取得結果からLinked Issueの情報を取得すればOKだと思いきやここで問題発生です。Pull Requestの取得結果の中にLinked Issueについてのプロパティがなぜか見当たりません。どうやらAPIで取得したPull RequestのデータにはLinked Issueの情報が含まれないようです。
これに関してはGitHubのCommunityでも取り沙汰されていますが、現在もなお実装に至っていないようです。
仕方がないので.body
のclose #{pull number}
の文字列からgrepを使用して取得します。
$ echo ${result} | jq .body -r | grep "close #[0-9]\+" | grep -o "[0-9]\+" 23
取得できました。
おわりに
GitHub API v3で指定のPull RequestのLinked Issueを取得してみました。
Linked Issueの情報はGUI上では表示されているのに、APIで取得した結果には含まれないというのは納得がいきませんが、スマートで無いながらも取得方法が見つけられて良かったです。
参考
以上